/* This provides whitelist-based filtering to sanitize HTML-flavored output.
 *  It has to have two separate object models for XHTML and HTML content.  The
 *  first is httpd's built-in XML.  For HTML, we use TidyLib.
 */

#include "mod_atom.h"

#include "buffio.h"
#include "tidy.h"

static int entity_value(char * name);

/* the following copied in from feedparser.py
 */
static char * acceptable_elements[] = {
    "a", "abbr", "acronym", "address", "area", "b", "big",
    "blockquote", "br", "button", "caption", "center", "cite", "code", "col",
    "colgroup", "dd", "del", "dfn", "dir", "div", "dl", "dt", "em", "fieldset",
    "font", "form", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "input",
    "ins", "kbd", "label", "legend", "li", "map", "menu", "ol", "optgroup",
    "option", "p", "pre", "q", "s", "samp", "select", "small", "span", "strike",
    "strong", "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th",
    "thead", "tr", "tt", "u", "ul", "var"
};
static char * acceptable_attributes[] = {
    "abbr", "accept", "accept-charset", "accesskey",
    "action", "align", "alt", "axis", "border", "cellpadding", "cellspacing",
    "char", "charoff", "charset", "checked", "cite", "class", "clear", "cols",
    "colspan", "color", "compact", "coords", "datetime", "dir", "disabled",
    "enctype", "for", "frame", "headers", "height", "href", "hreflang", "hspace",
    "id", "ismap", "label", "lang", "longdesc", "maxlength", "media", "method",
    "multiple", "name", "nohref", "noshade", "nowrap", "prompt", "readonly",
    "rel", "rev", "rows", "rowspan", "rules", "scope", "selected", "shape", "size",
    "span", "src", "start", "summary", "tabindex", "target", "title", "type",
    "usemap", "valign", "value", "vspace", "width"
};

static char * abs_uri_pattern = "^[a-zA-Z0-9][a-zA-Z0-9]*:";
static char * http_uri_pattern = "^[hH][tT][pP][pP][sS]?:";

static apr_hash_t * whitelist_e;
static apr_hash_t * whitelist_a;
static ap_regex_t * abs_uri_re;
static ap_regex_t * http_uri_re;

#define PRESENT "!"

/***** Tidy interface goo ************/

static void null_sink(void * sinData, byte bt) {
    /* tidy error messages go here */
}

static TidyOutputSink tidy_black_hole;

typedef struct {
    TidyAllocator base;
    apr_pool_t * pool;
    apr_hash_t * mem;
} alloc_bundle;

typedef struct {
    size_t size;
} size_rec;

static void * m_alloc(TidyAllocator * self, size_t size) {
    alloc_bundle * bundle = (alloc_bundle *) self;
    void * mem = apr_palloc(bundle->pool, size);
    size_rec * sr = (size_rec *) apr_palloc(bundle->pool, sizeof (size_rec));

    sr->size = size;
    apr_hash_set(bundle->mem, mem, sizeof (mem), sr);
    return mem;
}

static void * m_realloc(TidyAllocator * self, void * prev, size_t size) {
    if (!prev)
        return m_alloc(self, size);

    else {
        alloc_bundle * bundle = (alloc_bundle *) self;
        size_rec * sr = (size_rec *) apr_hash_get(bundle->mem, prev, sizeof (prev));
        char * mem = apr_palloc(bundle->pool, size);

        memcpy(mem, prev, sr->size);
        sr->size = size;
        apr_hash_set(bundle->mem, mem, sizeof (mem), sr);
        return mem;
    }
}

static void m_free(TidyAllocator * self, void * block) {
}

static void m_panic(TidyAllocator * self, ctmbstr msg) {
}

static const TidyAllocatorVtbl m_vtbl = {m_alloc, m_realloc, m_free, m_panic
};

/***** </Tidy> ***********************/

void atom_write_cdata(apr_text * text, genxWriter gw) {
    while (text) {
        genxAddText(gw, (utf8) text->text);
        text = text->next;
    }
}

/* decide whether to pass an XHTML element through.  Some attributes
 *  or subelements might be suppressed.  The following text is always
 *  passed through.
 */
static void filter_xhtml_element(char * * namespaces, apr_xml_elem * e,
                                 genxWriter gw, int html) {
    /* check whitelist */
    if ((strcmp(namespaces[e->ns], XHTML_NS) == 0) &&
            apr_hash_get(whitelist_e, e->name, APR_HASH_KEY_STRING)) {
        apr_xml_attr * a;
        apr_xml_elem * child;

        /* this element itself is OK */
        genxStartElementLiteral(gw, (html) ? NULL : (utf8) XHTML_NS,
                                (utf8) e->name);

        /* scan attributes */
        for (a = e->attr; a; a = a->next) {

            /* check whitelist */
            if ((a->ns != APR_XML_NS_NONE) ||
                    (apr_hash_get(whitelist_a, a->name, APR_HASH_KEY_STRING) == NULL))
                continue;

            /* this attribute is probably OK */

            /* check for href or src with non-http: schemes */
            if (strcmp(a->name, "href") == 0 || strcmp(a->name, "src") == 0) {

                int is_abs = (ap_regexec(abs_uri_re, a->value, 0, NULL, 0) == 0);
                int is_http = (ap_regexec(http_uri_re, a->value, 0, NULL, 0) == 0);

                if (is_abs && (!is_http))
                    continue;
            }

            genxAddAttributeLiteral(gw, NULL, (utf8) a->name, (utf8) a->value);
        }

        /* content */
        atom_write_cdata(e->first_cdata.first, gw);
        for (child = e->first_child; child; child = child->next)
            filter_xhtml_element(namespaces, child, gw, html);
        genxEndElement(gw);
    }

    /* trailing text */
    atom_write_cdata(e->following_cdata.first, gw);
}

/* Recursively filter/output HTML element nodes.
 */
static void filter_html_node(TidyDoc doc, TidyNode node, genxWriter gw,
                             TidyAllocator * alloc) {

    TidyNodeType t = tidyNodeGetType(node);

    /* only interested in text, start, & start-end (empty) nodes */
    if (t == TidyNode_Text) {
        TidyBuffer tbuf;
        char * html, * amp_at;

        tidyBufInitWithAllocator(&tbuf, alloc);
        tidyNodeGetText(doc, node, &tbuf);

        html = (char *) tbuf.bp;
        while ((amp_at = index(html, '&'))) {
            char * semi;
            int c;
            genxAddBoundedText(gw, (utf8) html, (utf8) amp_at);
            amp_at++;
            semi = index(amp_at, ';');
            if (!semi)
                return;
            *semi = 0;
            c = entity_value(amp_at);
            if (c != -1)
                genxAddCharacter(gw, c);
            html = semi + 1;
        }
        genxAddText(gw, (utf8) html);

        return;
    }
    if (t == TidyNode_Start || t == TidyNode_StartEnd) {
        char * name = (char *) tidyNodeGetName(node);
        TidyAttr a;
        TidyNode child;

        if (apr_hash_get(whitelist_e, name, APR_HASH_KEY_STRING) == NULL)
            return;

        /* in the whitelist */
        genxStartElementLiteral(gw, NULL, (utf8) name);

        /* scan attrs */
        for (a = tidyAttrFirst(node); a; a = tidyAttrNext(a)) {
            char * a_name = (char *) tidyAttrName(a);
            char * a_value = (char *) tidyAttrValue(a);

            if (!apr_hash_get(whitelist_a, tidyAttrName(a), APR_HASH_KEY_STRING))
                continue;

            /* attribute probably OK */
            if (strcmp(a_name, "href") == 0 || strcmp(a_name, "src") == 0) {
                int is_abs = (ap_regexec(abs_uri_re, a_value, 0, NULL, 0) == 0);
                int is_http = (ap_regexec(http_uri_re, a_value, 0, NULL, 0) == 0);

                if (is_abs && (!is_http))
                    continue;
            }

            genxAddAttributeLiteral(gw, NULL, (utf8) a_name, (utf8) a_value);
        }

        for (child = tidyGetChild(node); child; child = tidyGetNext(child))
            filter_html_node(doc, child, gw, alloc);
        genxEndElement(gw);
    }
}

/* Send the contents of an Atom Text Construct via the XML Writer.  There's
 *  a certain amount of Postel's law at work here; we silently ignore
 *  certain illegal practices.
 * The 'html' argument is true if we're pouring output into an HTML instance,
 *  in which case we don't put anything in the XHTML namespace.
 */
char * atom_filter_text_construct(apr_pool_t * pool, apr_xml_doc * doc,
                                  apr_xml_elem * e, genxWriter gw, int html) {
    text_construct_type_t type = atom_text_construct_type(e);

    if (type == TEXT_TYPE) {

        /* any child elements illegally appearing here are silently ignored.
         *  The first chunk of text after the start tag is all you'll see.
         */
        atom_write_cdata(e->first_cdata.first, gw);
    } else if (type == XHTML_TYPE) {
        /* find the xhtml:div, if there is one, and bypass it.
         * If some sicko sends me something like
         * <atom:title type="xhtml"><div><p>foo</p></div><p>bar</p></atom:title>
         * well then, only the first div will get looked at
         */
        apr_xml_elem * child = e->first_child;
        char * * namespaces = (char * *) doc->namespaces->elts;
        if (child &&
                (strcmp(namespaces[child->ns], XHTML_NS) == 0) &&
                (strcmp(child->name, "div") == 0))
            e = child;

        atom_write_cdata(e->first_cdata.first, gw);
        for (child = e->first_child; child; child = child->next)
            filter_xhtml_element(namespaces, child, gw, html);
    } else if (type == HTML_TYPE) {

        /* have to assemble the HTML into a glob of text to feed to Tidy */
        char * html = "";
        apr_text * text;
        TidyDoc tdoc;
        TidyNode body, child;
        alloc_bundle bundle;
        int status;

        /* any child elements illegally appearing here are silently ignored */
        for (text = e->first_cdata.first; text; text = text->next)
            html = apr_pstrcat(pool, html, text->text, NULL);

        html = apr_pstrcat(pool, "<html><head><title>x</title></head><body>",
                           html, "</body></html>", NULL);

        bundle.base.vtbl = &m_vtbl;
        bundle.pool = pool;
        bundle.mem = apr_hash_make(pool);

        tdoc = tidyCreateWithAllocator((TidyAllocator *) & bundle);
        tidySetErrorSink(tdoc, &tidy_black_hole);
        status = tidyParseString(tdoc, html);
        if (status < 0)
            return apr_psprintf(pool, "HTML Parse failure: %d", status);

        status = tidyCleanAndRepair(tdoc);
        if (status < 0)
            return apr_psprintf(pool, "HTML Cleanup failure: %d", status);

        /* can't just call filter_html_node on the body, because that's 
         *  (properly) not in the whitelist
         */
        body = tidyGetBody(tdoc);
        for (child = tidyGetChild(body); child; child = tidyGetNext(child))
            filter_html_node(tdoc, child, gw, (TidyAllocator *) & bundle);
    }

    return NULL;
}

void atom_filter_init(apr_pool_t * pool) {
    int i;

    whitelist_e = apr_hash_make(pool);
    for (i = 0; acceptable_elements[i]; i++)
        apr_hash_set(whitelist_e, acceptable_elements[i], APR_HASH_KEY_STRING,
                     PRESENT);
    whitelist_a = apr_hash_make(pool);
    for (i = 0; acceptable_attributes[i]; i++)
        apr_hash_set(whitelist_a, acceptable_attributes[i], APR_HASH_KEY_STRING,
                     PRESENT);

    abs_uri_re = ap_pregcomp(pool, abs_uri_pattern, AP_REG_EXTENDED);
    http_uri_re = ap_pregcomp(pool, http_uri_pattern, AP_REG_EXTENDED);

    tidy_black_hole.sinkData = NULL;
    tidy_black_hole.putByte = null_sink;
}

typedef struct entity_t {
    char * name;
    int value;
} entity_t;

#define ENTITY_COUNT 253
static entity_t entities[ENTITY_COUNT] = {
    { "AElig", 198
    },
    { "Aacute", 193
    },
    { "Acirc", 194
    },
    { "Agrave", 192
    },
    { "Alpha", 913
    },
    { "Aring", 197
    },
    { "Atilde", 195
    },
    { "Auml", 196
    },
    { "Beta", 914
    },
    { "Ccedil", 199
    },
    { "Chi", 935
    },
    { "Dagger", 8225
    },
    { "Delta", 916
    },
    { "ETH", 208
    },
    { "Eacute", 201
    },
    { "Ecirc", 202
    },
    { "Egrave", 200
    },
    { "Epsilon", 917
    },
    { "Eta", 919
    },
    { "Euml", 203
    },
    { "Gamma", 915
    },
    { "Iacute", 205
    },
    { "Icirc", 206
    },
    { "Igrave", 204
    },
    { "Iota", 921
    },
    { "Iuml", 207
    },
    { "Kappa", 922
    },
    { "Lambda", 923
    },
    { "Mu", 924
    },
    { "Ntilde", 209
    },
    { "Nu", 925
    },
    { "OElig", 338
    },
    { "Oacute", 211
    },
    { "Ocirc", 212
    },
    { "Ograve", 210
    },
    { "Omega", 937
    },
    { "Omicron", 927
    },
    { "Oslash", 216
    },
    { "Otilde", 213
    },
    { "Ouml", 214
    },
    { "Phi", 934
    },
    { "Pi", 928
    },
    { "Prime", 8243
    },
    { "Psi", 936
    },
    { "Rho", 929
    },
    { "Scaron", 352
    },
    { "Sigma", 931
    },
    { "THORN", 222
    },
    { "Tau", 932
    },
    { "Theta", 920
    },
    { "Uacute", 218
    },
    { "Ucirc", 219
    },
    { "Ugrave", 217
    },
    { "Upsilon", 933
    },
    { "Uuml", 220
    },
    { "Xi", 926
    },
    { "Yacute", 221
    },
    { "Yuml", 376
    },
    { "Zeta", 918
    },
    { "aacute", 225
    },
    { "acirc", 226
    },
    { "acute", 180
    },
    { "aelig", 230
    },
    { "agrave", 224
    },
    { "alefsym", 8501
    },
    { "alpha", 945
    },
    { "amp", 38
    },
    { "and", 8743
    },
    { "ang", 8736
    },
    { "apos", 39
    },
    { "aring", 229
    },
    { "asymp", 8776
    },
    { "atilde", 227
    },
    { "auml", 228
    },
    { "bdquo", 8222
    },
    { "beta", 946
    },
    { "brvbar", 166
    },
    { "bull", 8226
    },
    { "cap", 8745
    },
    { "ccedil", 231
    },
    { "cedil", 184
    },
    { "cent", 162
    },
    { "chi", 967
    },
    { "circ", 710
    },
    { "clubs", 9827
    },
    { "cong", 8773
    },
    { "copy", 169
    },
    { "crarr", 8629
    },
    { "cup", 8746
    },
    { "curren", 164
    },
    { "dArr", 8659
    },
    { "dagger", 8224
    },
    { "darr", 8595
    },
    { "deg", 176
    },
    { "delta", 948
    },
    { "diams", 9830
    },
    { "divide", 247
    },
    { "eacute", 233
    },
    { "ecirc", 234
    },
    { "egrave", 232
    },
    { "empty", 8709
    },
    { "emsp", 8195
    },
    { "ensp", 8194
    },
    { "epsilon", 949
    },
    { "equiv", 8801
    },
    { "eta", 951
    },
    { "eth", 240
    },
    { "euml", 235
    },
    { "euro", 8364
    },
    { "exist", 8707
    },
    { "fnof", 402
    },
    { "forall", 8704
    },
    { "frac12", 189
    },
    { "frac14", 188
    },
    { "frac34", 190
    },
    { "frasl", 8260
    },
    { "gamma", 947
    },
    { "ge", 8805
    },
    { "gt", 62
    },
    { "hArr", 8660
    },
    { "harr", 8596
    },
    { "hearts", 9829
    },
    { "hellip", 8230
    },
    { "iacute", 237
    },
    { "icirc", 238
    },
    { "iexcl", 161
    },
    { "igrave", 236
    },
    { "image", 8465
    },
    { "infin", 8734
    },
    { "int", 8747
    },
    { "iota", 953
    },
    { "iquest", 191
    },
    { "isin", 8712
    },
    { "iuml", 239
    },
    { "kappa", 954
    },
    { "lArr", 8656
    },
    { "lambda", 955
    },
    { "lang", 9001
    },
    { "laquo", 171
    },
    { "larr", 8592
    },
    { "lceil", 8968
    },
    { "ldquo", 8220
    },
    { "le", 8804
    },
    { "lfloor", 8970
    },
    { "lowast", 8727
    },
    { "loz", 9674
    },
    { "lrm", 8206
    },
    { "lsaquo", 8249
    },
    { "lsquo", 8216
    },
    { "lt", 60
    },
    { "macr", 175
    },
    { "mdash", 8212
    },
    { "micro", 181
    },
    { "middot", 183
    },
    { "minus", 8722
    },
    { "mu", 956
    },
    { "nabla", 8711
    },
    { "nbsp", 160
    },
    { "ndash", 8211
    },
    { "ne", 8800
    },
    { "ni", 8715
    },
    { "not", 172
    },
    { "notin", 8713
    },
    { "nsub", 8836
    },
    { "ntilde", 241
    },
    { "nu", 957
    },
    { "oacute", 243
    },
    { "ocirc", 244
    },
    { "oelig", 339
    },
    { "ograve", 242
    },
    { "oline", 8254
    },
    { "omega", 969
    },
    { "omicron", 959
    },
    { "oplus", 8853
    },
    { "or", 8744
    },
    { "ordf", 170
    },
    { "ordm", 186
    },
    { "oslash", 248
    },
    { "otilde", 245
    },
    { "otimes", 8855
    },
    { "ouml", 246
    },
    { "para", 182
    },
    { "part", 8706
    },
    { "permil", 8240
    },
    { "perp", 8869
    },
    { "phi", 966
    },
    { "pi", 960
    },
    { "piv", 982
    },
    { "plusmn", 177
    },
    { "pound", 163
    },
    { "prime", 8242
    },
    { "prod", 8719
    },
    { "prop", 8733
    },
    { "psi", 968
    },
    { "quot", 34
    },
    { "rArr", 8658
    },
    { "radic", 8730
    },
    { "rang", 9002
    },
    { "raquo", 187
    },
    { "rarr", 8594
    },
    { "rceil", 8969
    },
    { "rdquo", 8221
    },
    { "real", 8476
    },
    { "reg", 174
    },
    { "rfloor", 8971
    },
    { "rho", 961
    },
    { "rlm", 8207
    },
    { "rsaquo", 8250
    },
    { "rsquo", 8217
    },
    { "sbquo", 8218
    },
    { "scaron", 353
    },
    { "sdot", 8901
    },
    { "sect", 167
    },
    { "shy", 173
    },
    { "sigma", 963
    },
    { "sigmaf", 962
    },
    { "sim", 8764
    },
    { "spades", 9824
    },
    { "sub", 8834
    },
    { "sube", 8838
    },
    { "sum", 8721
    },
    { "sup", 8835
    },
    { "sup1", 185
    },
    { "sup2", 178
    },
    { "sup3", 179
    },
    { "supe", 8839
    },
    { "szlig", 223
    },
    { "tau", 964
    },
    { "there4", 8756
    },
    { "theta", 952
    },
    { "thetasym", 977
    },
    { "thinsp", 8201
    },
    { "thorn", 254
    },
    { "tilde", 732
    },
    { "times", 215
    },
    { "trade", 8482
    },
    { "uArr", 8657
    },
    { "uacute", 250
    },
    { "uarr", 8593
    },
    { "ucirc", 251
    },
    { "ugrave", 249
    },
    { "uml", 168
    },
    { "upsih", 978
    },
    { "upsilon", 965
    },
    { "uuml", 252
    },
    { "weierp", 8472
    },
    { "xi", 958
    },
    { "yacute", 253
    },
    { "yen", 165
    },
    { "yuml", 255
    },
    { "zeta", 950
    },
    { "zwj", 8205
    },
    { "zwnj", 8204
    }
};

static int entity_value(char * name) {
    int hi = ENTITY_COUNT, lo = -1;

    while ((hi - lo) > 1) {
        int probe = (hi + lo) / 2;
        if (strcmp(entities[probe].name, name) > 0)
            hi = probe;
        else
            lo = probe;
    }
    if (lo == -1 || strcmp(entities[lo].name, name) != 0)
        return -1;
    return entities[lo].value;
}
